import os
import sys
import json
import time
import datetime
import requests
from selenium.webdriver.edge.options import Options
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from parsel import Selector

from paddlenlp.data import JiebaTokenizer, Pad, Vocab
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
    QPushButton, QTextEdit, QProgressBar, QGridLayout, QFrame, QScrollArea,QMessageBox
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QPixmap, QIcon


class BilibiliDanmakuMonitor(QThread):
    update_signal = pyqtSignal(str)  # 用于更新GUI的信号
    finished_signal = pyqtSignal()  # 用于通知监控结束的信号

    def __init__(self, roomid, api_url,cookie_path, token=None, headless=True):
        """
        初始化BilibiliDanmakuMonitor对象

        :param roomid: B站直播间ID
        :param cookie_path: Cookie文件路径
        :param headless: 是否使用无头模式运行浏览器
        """
        super().__init__()
        # 房间号等信息
        self.roomid = roomid
        self.url = f"https://live.bilibili.com/{roomid}"
        self.api_url= api_url
        self.token = token

        # 存储弹幕的相关信息
        self.dm_dict = {}
        self.non_violation_dict = {}
        self.violation_dict = {}
        self.violation_count = {}
        self.dm_num = 0
        self.dm_id = 0
        self.dm_cur_time = time.time()
        self.dm_interval_avg = 3.0
        self.dm_interval_sum = 0

        # 配置Selenium WebDriver
        opt = Options()
        opt.add_argument("--disable-webrtc-multiple_routes")
        opt.add_argument("--disable-webrtc-hide-local-ips-with-mdns")
        opt.add_argument("--disable-extensions")
        opt.add_argument("--disable-web-security")
        opt.add_argument("--disable-features=WebRtcHideLocalIpsWithMdns")
        opt.add_argument('--no-sandbox')
        opt.add_argument("--log-level=4")
        opt.add_argument('--disable-gpu')
        opt.add_argument("--mute-audio")
        if headless:
            opt.add_argument('--headless')

        self.driver = webdriver.Edge(options=opt)
        self.driver.implicitly_wait(10)

        # 加载cookies
        with open(cookie_path, 'r') as f:
            self.cookies = json.loads(f.read())

    @staticmethod
    def time_convert(send_time):
        """将时间戳转换为时间"""
        return datetime.datetime.fromtimestamp(int(send_time))
    
    def get_result(self, token, text):
        """通过API获取结果。"""
        if token:
            headers = {
                "Authorization": f"token {self.token}",
                "Content-Type": "application/json"
                }
        else:
            headers = {
                "Content-Type": "application/json"
                }
        data = {"data": {"text": text}}
        r = requests.post(url=self.api_url, headers=headers, data=json.dumps(data))
        return json.loads(r.text)['result']
        

    def add_cookies(self):
        """将cookies添加到浏览器中"""
        self.driver.delete_all_cookies()
        for cookie in self.cookies:
            self.driver.add_cookie({
                'domain': '.bilibili.com',
                'name': cookie['name'],
                'value': cookie['value'],
                'path': '/',
                'expires': None,
                'httponly': False,
            })
        self.driver.refresh()

    def run(self):
        """监控弹幕（在新线程中运行）"""
        self.monitor_danmaku()

    def monitor_danmaku(self):
        """监控弹幕"""
        self.driver.get(self.url)

        # 检查目标URL是否正确
        try:
            self.update_signal.emit("检测目标URL是否正确...")
            self.driver.find_element(By.CSS_SELECTOR, ".lite-room>div>iframe:nth-child(1)").get_attribute("src")
            true_url = self.driver.find_element(By.CSS_SELECTOR, ".lite-room>div>iframe:nth-child(1)").get_attribute(
                "src")
            self.driver.get(url=true_url)
            self.update_signal.emit("已经更换正确目标URL")
        except Exception as e:
            self.update_signal.emit("URL正确！开始进行实时监控")

        # 获取当前直播间的所有信息
        zb_anchor = self.driver.find_elements(By.CSS_SELECTOR, ".left-ctnr.left-header-area>a")
        if zb_anchor: zb_anchor = zb_anchor[0].get_attribute("title")
        zb_title = self.driver.find_elements(By.CSS_SELECTOR, ".flex-wrap>div")
        if zb_title: zb_title = zb_title[0].get_attribute("title")
        zb_live_area = self.driver.find_elements(By.CSS_SELECTOR, ".live-area>a")
        if zb_live_area: zb_live_area = zb_live_area[0].get_attribute("title")
        
        self.update_signal.emit("=" * 100)
        self.update_signal.emit("正在监控直播间!")
        self.update_signal.emit(f"直播间主播：{zb_anchor}")
        self.update_signal.emit(f"直播间标题：{zb_title}")
        self.update_signal.emit(f"直播间分区：{zb_live_area}")

        # 绕过自动化检测
        self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
            "source": """
            Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
            })
            """
        })

        self.add_cookies()

        try:
            while True:
                # 循环监控
                selectors = Selector(self.driver.page_source)

                # 显式等待10秒，检查是否存在直播视频标签
                try:
                    WebDriverWait(self.driver, 10).until(
                        EC.presence_of_element_located(
                            (By.CSS_SELECTOR, "#live-player-ctnr>div .live-player-mounter.h-100>video"))
                    )
                    live_status = True
                except:
                    live_status = False

                if not live_status:
                    self.update_signal.emit("直播已结束")
                    break

                # 弹幕信息标签列表
                dm_list = selectors.css("#chat-items .chat-item.danmaku-item")

                for dm_n in range(self.dm_id, len(dm_list)):
                    # 取出弹幕
                    dm = dm_list[dm_n]
                    # 弹幕时间
                    dm_time = dm.css("::attr(data-ts)").get()
                    # 弹幕发送人昵称
                    dm_send_name = dm.css("::attr(data-uname)").get()
                    # 弹幕发送人uid
                    dm_send_uid = dm.css("::attr(data-uid)").get()
                    # 弹幕内容
                    dm_send_content = dm.css("::attr(data-danmaku)").get()
                    message_dict = {
                        "dm_send_time": str(self.time_convert(dm_time)),
                        "dm_send_name": dm_send_name,
                        "dm_send_uid": dm_send_uid,
                        "dm_send_content": dm_send_content
                    }
                    # 获取模型的推理结果
                    results = self.get_result(text=dm_send_content, token=self.token)
                    # 获取模型推理的标签
                    label = results[0]['predictions'][0]['label']
                    # 获取模型推理的分数
                    score = results[0]['predictions'][0]['score']

                    self.dm_id += 1
                    # 违规信息处理
                    if label == "违规" and not self.violation_dict.get(f"{dm_time}-{dm_send_name}-{dm_send_uid}-{dm_send_content}"):
                        if dm_send_uid not in self.violation_count:
                            self.violation_count[dm_send_uid] = 1
                        else:
                            self.violation_count[dm_send_uid] += 1
                        self.violation_dict[f"{dm_time}-{dm_send_name}-{dm_send_uid}-{dm_send_content}"] = message_dict
                        self.violation_dict[f"{dm_time}-{dm_send_name}-{dm_send_uid}-{dm_send_content}"]['score'] = score
                   
                   # 非违规信息处理
                    if label != "违规" and not self.non_violation_dict.get(f"{dm_time}-{dm_send_name}-{dm_send_uid}-{dm_send_content}"):
                        self.non_violation_dict[f"{dm_time}-{dm_send_name}-{dm_send_uid}-{dm_send_content}"] = message_dict

                    if not self.dm_dict.get(f"{dm_time}-{dm_send_name}-{dm_send_uid}-{dm_send_content}"):
                        # 弹幕数量增加
                        self.dm_num += 1
                        # 更新时间
                        self.update_signal.emit("=" * 100)
                        
                        if label == "违规":
                            self.update_signal.emit("🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫")
                            self.update_signal.emit(f"{dm_send_name}({dm_send_uid})已经违规了！！！，总计违规次数{self.violation_count[dm_send_uid]}")
                                
                        self.update_signal.emit(f"第{self.dm_num}个弹幕")
                        self.update_signal.emit(f"{dm_send_name}({int(dm_send_uid)})：{dm_send_content}")
                        self.update_signal.emit(f"发送时间：{self.time_convert(dm_time)}")
                        self.update_signal.emit(f'模型预测结果：{label}')
                        self.update_signal.emit(f'模型预测分数：{score}')
                        self.dm_dict[f"{dm_time}-{dm_send_name}-{dm_send_uid}-{dm_send_content}"] = message_dict
                        # 从第二个弹幕开始算间隔的平均时间
                        if self.dm_num >= 2:
                            self.dm_interval_sum += time.time() - self.dm_cur_time
                            self.dm_interval_avg = self.dm_interval_sum / float(self.dm_num - 1)
                            self.update_signal.emit(f"弹幕时间间隔总时间：{self.dm_interval_sum}")
                            self.update_signal.emit(f"弹幕时间间隔平均时间：{self.dm_interval_avg}")
                            
                            if self.dm_interval_avg < 1.0:
                                self.dm_interval_avg = 3.0

                        # 更新时间
                        self.dm_cur_time = time.time()
                
                # 检查弹幕更新是否超过设定阈值
                if time.time() - self.dm_cur_time > min(5.0 * self.dm_interval_avg, float(15)) and self.dm_num >= 2:
                    self.update_signal.emit("=" * 100) 
                    self.update_signal.emit("很长时间已经没有更新弹幕了，网页刷新中...")
                    self.driver.refresh()
                    # 刷新dm_id
                    self.dm_id = 0
                    # 重置时间
                    self.dm_cur_time = time.time()
        finally:
            self.driver.quit()
            self.finished_signal.emit()

    def save_results(self):
        """保存监控结果"""
        self.update_signal.emit("直播监控结束，最终结果如下：")

        self.update_signal.emit("\n违规弹幕统计：")
        sorted_violation = sorted(self.violation_count.items(), key=lambda item: item[1], reverse=True)
        for uid, count in sorted_violation:
            user_info = list(filter(lambda x: x.split('-')[-2] == uid, self.violation_dict.keys()))
            if user_info:
                user_key = user_info[0]
                user_name = self.violation_dict[user_key]['dm_send_name']
                self.update_signal.emit(f"{user_name}({uid}) - 违规次数：{count}")

        self.update_signal.emit(f"\n未违规弹幕总数：{len(self.non_violation_dict)}")
        self.update_signal.emit(f"违规弹幕总数：{len(self.violation_dict)}")
        self.update_signal.emit(f"弹幕统计总数：{self.dm_num}")

        # 创建对应房间号的文件夹
        if not os.path.exists(str("data/" + self.roomid)):
            # 如果文件夹不存在，则创建文件夹
            os.makedirs("data/" + str(self.roomid))

        # 保存未违规弹幕到文本文件
        with open("data/" + f'{self.roomid}/non_violation_danmaku.txt', 'w', encoding='utf-8') as f:
            for key, value in self.non_violation_dict.items():
                f.write(value["dm_send_content"] + "\n")

        # 保存违规弹幕到文本文件
        with open("data/" + f'{self.roomid}/violation_danmaku.txt', 'w', encoding='utf-8') as f:
            for key, value in self.violation_dict.items():
                f.write(value["dm_send_content"] + "\n")


class MainWindow(QMainWindow):
    """
    主窗口类，继承自QMainWindow。
    负责管理和显示应用程序的主窗口，包括用户界面布局、交互逻辑和样式设置。
    """

    def __init__(self):
        """
        构造函数，初始化主窗口的UI组件。
        通过调用 init_ui 函数来设置窗口的标题、图标、大小等基本属性，
        并创建用户界面的布局和功能组件。
        """
        super().__init__()
        self.init_ui()

    def init_ui(self):
        """
        初始化用户界面，包括创建输入框、按钮和显示区域。
        使用垂直布局（QVBoxLayout）来组织界面，使其简洁直观。
        还为每个控件设置了样式，以提升美观性。
        """
        # 设置窗口标题
        self.setWindowTitle('B站直播弹幕监控')
        # 设置窗口初始位置和大小
        self.setGeometry(100, 100, 800, 600)
        # 设置窗口图标
        self.setWindowIcon(QIcon('image/icon.png'))

        # 创建中央小部件和布局
        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        # 使用 QVBoxLayout 实现垂直布局
        layout = QVBoxLayout()
        central_widget.setLayout(layout)

        # 创建房间号、API地址和 Token 的输入框及其布局
        input_layout = QHBoxLayout()

        # 房间号输入框，提供提示文本
        self.room_input = QLineEdit()
        self.room_input.setPlaceholderText("请输入房间号")
        input_layout.addWidget(QLabel("房间号:"))
        input_layout.addWidget(self.room_input)

        # API地址输入框，设置默认地址并提供提示文本
        self.api_input = QLineEdit()
        self.api_input.setText("http://127.0.0.1:8189/taskflow/cls")
        self.api_input.setPlaceholderText("请输入API地址")
        input_layout.addWidget(QLabel("API:"))
        input_layout.addWidget(self.api_input)

        # Token输入框，提供提示文本
        self.token_input = QLineEdit()
        self.token_input.setPlaceholderText("(可选)百度API服务必须输入！")
        input_layout.addWidget(QLabel("Token:"))
        input_layout.addWidget(self.token_input)

        # 将输入布局添加到主布局中
        layout.addLayout(input_layout)

        # 创建并添加“开始监控”按钮，绑定点击事件
        self.start_button = QPushButton('开始监控')
        self.start_button.clicked.connect(self.start_monitoring)
        layout.addWidget(self.start_button)

        # 显示直播间信息的标签，设置字体大小和样式
        self.streamer_info_label = QLabel("直播间信息:")
        self.streamer_info_label.setStyleSheet("font-size: 16px; font-weight: bold;")
        layout.addWidget(self.streamer_info_label)

        # 创建显示主播、标题和分区信息的布局和标签
        self.streamer_info_widget = QWidget()
        self.streamer_info_layout = QHBoxLayout()
        self.streamer_info_widget.setLayout(self.streamer_info_layout)

        self.anchor_info = QLabel("主播: N/A")
        self.anchor_info.setStyleSheet("font-size: 16px; font-weight: bold;")
        self.title_info = QLabel("标题: N/A")
        self.title_info.setStyleSheet("font-size: 16px; font-weight: bold;")
        self.area_info = QLabel("分区: N/A")
        self.area_info.setStyleSheet("font-size: 16px; font-weight: bold;")
        self.streamer_info_layout.addWidget(self.anchor_info)
        self.streamer_info_layout.addWidget(self.title_info)
        self.streamer_info_layout.addWidget(self.area_info)
        self.setLayout(self.streamer_info_layout)

        # 将显示信息的布局添加到主布局中
        layout.addWidget(self.streamer_info_widget)

        # 创建并添加显示弹幕内容的文本区域，设置为只读
        self.dm_area = QTextEdit()
        self.dm_area.setReadOnly(True)
        layout.addWidget(self.dm_area)

        # 设置应用程序的全局样式，提升界面美观性
        self.setStyleSheet("""
            QWidget {
                font-size: 14px;
                background-color: #f0f0f0;
            }
            QLabel {
                font-size: 14px;
                color: #333;
            }
            QLineEdit, QTextEdit {
                padding: 8px;
                border: 1px solid #ccc;
                border-radius: 5px;
            }
            QPushButton {
                background-color: #5cb85c;
                color: white;
                padding: 10px;
                border-radius: 5px;
            }
            QPushButton:disabled {
                background-color: #ccc;
            }
        """)
    def show_input_error_dialog(self, message):
        """
        显示输入错误的弹窗。
        """
        msg_box = QMessageBox()
        msg_box.setIcon(QMessageBox.Warning)
        msg_box.setText(message)
        msg_box.setWindowTitle("错误")
        msg_box.setStandardButtons(QMessageBox.Ok)
        # 设置弹窗的图标
        msg_box.setWindowIcon(QIcon('image/icon.png'))
        msg_box.exec_()
    def start_monitoring(self):
        """
        开始监控函数，用于处理按钮点击事件。
        首先获取用户输入的房间号、API地址和Token，然后检查输入的有效性。
        如果输入有效，则禁用“开始监控”按钮，并启动一个新的线程进行直播间弹幕的监控。
        """
        if not os.path.exists("jsoncookie.json"):
            self.show_input_error_dialog("请先启动get_cookie.py获取cookie文件！")
            return
            
        # 获取房间号、API地址和Token
        roomid = self.room_input.text()
        api_url = self.api_input.text()
        token = None

        # 清空弹幕区域的内容
        self.dm_area.clear()

        # 检查房间号和API地址是否输入
        if not roomid:
            self.show_input_error_dialog("请输入房间号！")
            return
        if not api_url:
            self.show_input_error_dialog("请输入API地址！")
            return
        
        if "baidu.com" in api_url:
            token = self.token_input.text()
            if not token:
                self.show_input_error_dialog("检测为百度的API服务，请输入Token！")
                return
                
        # 禁用开始按钮，防止重复点击
        self.start_button.setEnabled(False)

        # 创建并启动监控线程
        self.monitor_thread = BilibiliDanmakuMonitor(
            roomid=roomid, cookie_path='jsoncookie.json',
            api_url=api_url, token=token
        )
        # 将线程的信号连接到日志更新和监控完成函数
        self.monitor_thread.update_signal.connect(self.update_log)
        self.monitor_thread.finished_signal.connect(self.monitoring_finished)
        self.monitor_thread.start()

    def update_log(self, message):
        """更新日志区域"""
        # 根据消息内容更新不同的信息
        if "直播间主播" in message:
            self.anchor_info.setText("主播: " + message.split("：")[1])
        elif "直播间标题" in message:
            self.title_info.setText("标题: " + message.split("：")[1])
        elif "直播间分区" in message:
            self.area_info.setText("分区: " + message.split("：")[1])
        else:
            self.dm_area.append(message)

    def monitoring_finished(self):
        """
        监控完成函数，在监控线程结束时调用。
        该函数用于保存监控结果并重新启用“开始监控”按钮。
        """
        self.monitor_thread.save_results()  # 保存结果
        self.start_button.setEnabled(True)  # 重新启用按钮
        self.dm_area.append("监控结束，结果已保存。")


if __name__ == "__main__":
    """
    主程序入口，创建应用程序并启动主窗口。
    当程序运行时，Qt事件循环保持活跃，直到用户关闭窗口或程序结束。
    """
    # 创建Qt应用程序实例
    app = QApplication(sys.argv)
    # 创建主窗口实例
    window = MainWindow()
    # 显示主窗口
    window.show()
    # 进入Qt事件循环，等待用户交互
    sys.exit(app.exec_())